package org.pixelami.twisted.spread.pb
{
	import flash.events.Event;
	
	import org.pixelami.twisted.errors.JellyableError;
	import org.pixelami.twisted.failure.IFailure;
	import org.pixelami.twisted.internet.defer.Deferred;
	import org.pixelami.twisted.spread.errors.DeadReferenceError;
	import org.pixelami.twisted.spread.jelly.Jelly;
	import org.pixelami.twisted.spread.jelly.Jellyable;

	
	public class AMFBroker extends PeanutButter
	{
		public static var MAX_BROKER_REFS:uint = 100;//?
		
		private var version:uint = 6;
		
		private var _currentLocalId:uint = 0;
		
		private var _currentRequestId:uint = 0;
		
		public var disconnected:int = 0;
		private var disconnects:Array;
		private var failures:Array;
		private var connects:Array;
		private var localObjects:Object;
		private var security:Object;
		private var pageProducers:Array;
		private var currentRequestID:int;
		private var currentLocalID:int;
		
		private var luids:Object;
		private var remotelyCachedObjects:Object;
		private var remotelyCachedLUIDs:Object;
		private var locallyCachedObjects:Object;
		private var waitingForAnswers:Object;
		
		private var serializingPerspective:Object;
		public var unserializingPerspective;
		private var jellyMethod:Object;
		private var jellyArgs:Object;
		private var jellyKw:Object;
		private var maxBrokerRefsViolations = 0;
		
		public var factory:Object;

		public function AMFBroker()
		{
			this.disconnected = 0;
	        this.disconnects = [];
	        this.failures = [];
	        this.connects = [];
	        this.localObjects = {};
	        this.security = security;
	        this.pageProducers = [];
	        this.currentRequestID = 0;
	        this.currentLocalID = 0;
	        
	        /*
	        # Some terms:
	        #  PUID: process unique ID; return value of id() function.  type "int".
	        #  LUID: locally unique ID; an ID unique to an object mapped over this
	        #        connection. type "int"
	        #  GUID: (not used yet) globally unique ID; an ID for an object which
	        #        may be on a redirected or meta server.  Type as yet undecided.
	        # Dictionary mapping LUIDs to local objects.
	        # set above to allow root object to be assigned before connection is made
	        # this.localObjects = {}
	        # Dictionary mapping PUIDs to LUIDs.
	        */
	        this.luids = {}
	        /*
	        # Dictionary mapping LUIDs to local (remotely cached) objects. Remotely
	        # cached means that they're objects which originate here, and were
	        # copied remotely.
	        */
	        this.remotelyCachedObjects = {}
	        /*
	        # Dictionary mapping PUIDs to (cached) LUIDs
	        */
	        this.remotelyCachedLUIDs = {}
	        /*
	        # Dictionary mapping (remote) LUIDs to (locally cached) objects.
	        */
	        this.locallyCachedObjects = {}
	        this.waitingForAnswers = {}
		}
		
		public function  serialize(object, perspective=null, method=null, args=null, kw=null)
		{
	        /*
	        Jelly an object according to the remote security rules for this broker.
	        */
	
	        if(object is Deferred)
	        {
	            object.addCallbacks(this.serialize, function(x){},
	                                
	                [perspective,
	                method,
	                args,
	                kw
	                ])
	            return object
	        }
			
			/*
	        # XXX This call is NOT REENTRANT and testing for reentrancy is just
	        # crazy, so it likely won't be.  Don't ever write methods that call the
	        # broker's serialize() method recursively (e.g. sending a method call
	        # from within a getState (this causes concurrency problems anyway so
	        # you really, really shouldn't do it))
			*/
	        
	        //# this.jellier = _NetJellier(this)
	        this.serializingPerspective = perspective;
	        this.jellyMethod = method;
	        this.jellyArgs = args;
	        this.jellyKw = kw;
	        try
	        {
	            //trace("Jellying");
	            return Jelly.jelly(object, this.security, null, this);
	        }
	        finally
	        {
	            this.serializingPerspective = null;
	            this.jellyMethod = null;
	            this.jellyArgs = null;
	            this.jellyKw = null;
	        }
	 	}
	
	    public function unserialize(sexp, perspective = null):Object
	    {
	        /*
	        Unjelly an sexp according to the local security rules for this broker.
	        */
	
	        this.unserializingPerspective = perspective
	        try
	        {
	            return Jelly.unjelly(sexp, this.security, null, this)
	        }
	        catch(e:Error)
	        {
	        	//trace("unjelly error - "+e);
	        }
	        finally
	        {
	            this.unserializingPerspective = null;
	        }
	        return null
	    }
	    
	    /*
	    override public function dataReceived(obj:*):void
		{
			//trace("Broker dataReceived obj - "+obj);
			if(currentDialect)
			{
				expressionReceived(obj);
			}
			else
			{
				for each(var dialect:String in knowDialects)
				{
					for each(var d:String in obj)
					{
						if(d == dialect) 
						{
							setDialect(d);
							transport.writeUTFBytes(d);
							transport.flush();
							break;
						}
					}
				}
				
			}
		}
		
		
		private function setDialect(dialect:String):void
		{
			currentDialect = dialect;
			//trace("currentDialect = "+currentDialect);
			
		}
		
		private function sendEncoded(obj:Object):void
		{
			var ba:ByteArray = new ByteArray();
			ba.objectEncoding = ObjectEncoding.AMF0;
			
			writeList(obj,ba);
			
			prettyPrint(ba);
			
			writeBytes(ba);
			flush();
		}
		*/
		public function connectionReady():void
		{
			dispatchEvent(new Event(Event.CONNECT));
		}
		
		override public function expressionReceived(obj:Object):void
		{
			//trace("incoming sexp - "+obj);
			// this should be an array with item[0] being the command
			var a:Array = obj as Array;
			
			var fn:Function = this["proto_"+obj[0]];
			
			var args:Array = a.splice(1);
			
			fn.apply(this,args);
		}
		
		protected function proto_version(v:uint):void
		{
			//trace("version "+v);
			if(v == version) 
			{
				
				sendEncoded(new Array('version',6));
				
				connectionReady();
			}
		}
		
		private function proto_error(requestID, fail):void
		{
			trace("proto_error - "+requestID);
			trace(fail);
		}
		
		private function proto_answer(requestID, netResult):void
		{
			var d:Deferred = this.waitingForAnswers[requestID];
			//trace("proto_answer - d id : "+d.ref_id);
			//this.waitingForAnswers[requestID].__del__();
        	delete this.waitingForAnswers[requestID];
        	//this.waitingForAnswers[requestID] = null;
        	////trace("proto_answer - d POST delete "+d.ref_id);
        	
        	var cbargs = this.unserialize(netResult);
        	//trace("netresult = "+cbargs);
        	d.callback(cbargs)
		}
		
		public function  sendDecRef(objectID)
		{
	        /*
	        """(internal) Send a DECREF directive.
	        """
	        */
	        this.sendCall("decref", objectID);
	 	}

		private function proto_decref(objectID)
		{
			/*
			"""(internal) Decrement the reference count of an object.
	
	        If the reference count is zero, it will free the reference to this
	        object.
	        """
	        */
	        
	        var refs = this.localObjects[objectID].decref();
	        //trace("proto_decref = "+refs);
	        if (refs == 0)
	        {
	            var puid = this.localObjects[objectID].object.processUniqueID();
	            delete this.luids[puid];
	            delete this.localObjects[objectID];
	        }
		}
		
		private function localObjectForID(luid)
		{
	        /*
	        """Get a local object for a locally unique ID.
	
	        I will return an object previously stored with
	        self.L{registerReference}, or C{None} if XXX:Unfinished thought:XXX
	        """
			*/
	        var lob = this.localObjects[luid];
	        if (!lob)
	            return;
	        return lob.object;
	 	}
		
		private function proto_message(requestID, objectID, message, answerRequired, netArgs, netKw):void
		{
			this._recvMessage(this.localObjectForID, requestID, objectID, message, answerRequired, netArgs, netKw);
		}
		
		private function  proto_cachemessage(requestID, objectID, message, answerRequired, netArgs, netKw)
		{
	        this._recvMessage(this.cachedLocallyAs, requestID, objectID, message, answerRequired, netArgs, netKw);
		}
		
		
		
	    private function _recvMessage(findObjMethod, requestID, objectID, message, answerRequired, netArgs, netKw)
	    {
	        /*
	        """Received a message-send.
	
	        Look up message based on object, unserialize the arguments, and
	        invoke it with args, and send an 'answer' or 'error' response.
	        """
	        */
	        var errorStatus:Boolean = false;
	        
	        try
	        {
	            var object = findObjMethod(objectID);
	            //trace("_recvMessage resolved object : "+object);
	            
	            if (!object)
	            {
	                throw new Error("Invalid Object ID");
	            }
	            var netResult = object.remoteMessageReceived(this, message, netArgs, netKw)
	        }
	        catch (e:JellyableError)
	        {
	            //trace("_recvMessage JellyableError "+e);
	            errorStatus = true;
	            if (answerRequired)
	            {
	            	
	            	/*
	                # If the error is Jellyable or explicitly allowed via our
	                # security options, send it back and let the code on the
	                # other end deal with unjellying.  If it isn't Jellyable,
	                # wrap it in a CopyableFailure, which ensures it can be
	                # unjellied on the other end.  We have to do this because
	                # all errors must be sent back.
	                */
	                if((e is Jellyable) || this.security.isClassAllowed(e))
	                {
	                    this._sendError(e, requestID);
	                }
	                else
	                {
	                    this._sendError(new CopyableFailure(e), requestID)
	                }
	            }
	        }
	        catch(e:Error)
	        {
	            //trace("_recvMessage Error "+e);
	            errorStatus = true;
	            if (answerRequired)
	            {
	                //log.msg("Peer will receive following PB //traceback:", isError=True)
	                //trace("Peer will receive following PB //traceback:");
	                var f = new CopyableFailure();
	                this._sendError(f, requestID);
	            }
	            //log.err();
	        }
	        
			if(!errorStatus)
	        {
	            if (answerRequired)
	            {
	                if (netResult is Deferred) 
	                {
	                    //var args = (requestID,);
	                    var args = new PyTuple(requestID);
	                    netResult.addCallbacks(this._sendAnswer, this._sendFailureOrError,
	                                           args, {},
	                                           args, {});
	                    //# XXX Should this be done somewhere else?
	                }
	                else
	                {
	                    this._sendAnswer(netResult, requestID)
	                }
	            }
	        }
	    }
	    /*
	    ##
	    # success
	    ##
		*/
	    private function _sendAnswer(netResult, requestID)
	    {
	    	/*
	        """(internal) Send an answer to a previously sent message.
	        """
	        */
	        this.sendCall("answer", requestID, netResult)
	    }
	    
	    //##
	    //# failure
	    //##
	    private function _sendFailureOrError(fail, requestID)
	    {
	        /*
	        """
	        Call L{_sendError} or L{_sendFailure}, depending on whether C{fail}
	        represents an L{Error} subclass or not.
	        """
	        */
	        if (fail.check(Error) == null)
	        {
	            this._sendFailure(fail, requestID);
	        }
	        else
	        {
	            this._sendError(fail, requestID);
	        }
	    }
	
	
	    private function  _sendFailure(fail, requestID)
	    {
	        /*
	        """Log error and then send it."""
	        */
	        //log.msg("Peer will receive following PB //traceback:")
	        //log.err(fail)
	        //trace("Peer will receive following PB //traceback:"+fail)
	        this._sendError(fail, requestID)
	    }
	
	    private function  _sendError(fail, requestID)
	    {
	        /*
	        """(internal) Send an error for a previously sent message.
	        """
	        */
	        if (fail is IFailure)
	        {
	            //# If the failures value is jellyable or allowed through security,
	            //# send the value
	            if ((fail.value is Jellyable) ||
	                this.security.isClassAllowed(fail.value.__class__))
	            {    
	                fail = fail.value;
	            }
	            else if (! (fail is CopyableFailure))
	            {
	                // arrgh what is factory ?
	                //fail = __static__.failure2Copyable(fail, this.factory.unsafetracebacks);
	            }
	        }
	        if (fail is CopyableFailure)
	        {
	            // arghh what is factory ?
	            //fail.unsafetracebacks = this.factory.unsafetracebacks;
	        }
	        this.sendCall("error", requestID, this.serialize(fail))
	    }

	    
	    private function cacheLocally(cid, instance)
	    {
	    	/*
	        """(internal)
	
	        Store a non-filled-out cached instance locally.
	        """
	        */
	        this.locallyCachedObjects[cid] = instance;
	    }

	    private function  cachedLocallyAs(cid)
	    {
	        var instance = this.locallyCachedObjects[cid];
	        return instance;
	    }
		
		
		private function newLocalID():uint
		{
			return ++_currentLocalId;
		}
		
		private function newRequestID():uint
		{
			return ++_currentRequestId;
		}
		
		public function sendMessage(prefix, perspective, objectID, message, args, kw):Deferred
		{
			
        	var pbc = null;
        	var pbe = null;
        	
        	var answerRequired:uint = 1;
        	
        	if(kw.hasOwnProperty('pbcallback'))
        	{
	            pbc = kw['pbcallback'];
	            delete kw['pbcallback'];
	        }
	            
	        if(kw.hasOwnProperty('pberrback'))
	        {
	            pbe = kw['pberrback'];
	            delete kw['pberrback'];
	        }
	            
	        if(kw.hasOwnProperty('pbanswer'))
	        {
	            if((!pbe) && (!pbc)) "You can't specify a no-answer requirement."
	            answerRequired = kw['pbanswer']
	            delete kw['pbanswer']
	        }
	        
	        if(this.disconnected)
	            throw new DeadReferenceError("Calling Stale Broker")
	        
	        
	        var netArgs;
	        var netKw;
	        var _tupArgs:PyTuple = new PyTuple();//.append(args);
	        _tupArgs.append(args);
	        try
	        {
	            netArgs = this.serialize(_tupArgs, perspective, message);
	            netKw = this.serialize(kw, perspective, message);
	        }
	        catch(e:Error)
	        {
	            //return defer.fail(new Failure())
	            //trace("bad serializing in Broker sendMessage - "+e);
	        }
	            
	        var requestID:uint = this.newRequestID()
	        var rval;
	        if(answerRequired)
	        {
	            rval = new Deferred()
	            this.waitingForAnswers[requestID] = rval
	            if(pbc || pbe)
	            {
	                //log.msg('warning! using deprecated "pbcallback"');
	                //trace('warning! using deprecated "pbcallback"');
	                rval.addCallbacks(pbc, pbe);
	            }
	        }
	        else
	        {
	            rval = null;
	        }
	        this.sendCall(prefix+"message", requestID, objectID, message, answerRequired, netArgs, netKw)
	        return rval
		}
		
		public function sendCall(...exp)
		{
	        /*
	        Utility method to send an expression to the other side of the connection.
	        */
	        //trace('sendCall - '+exp);
	        this.sendEncoded(exp)
	 	}
	 	
	 	public function remoteForName(name):RemoteReference
	 	{
	        /*
	        Returns an object from the remote name mapping.
	
	        Note that this does not check the validity of the name, only
	        creates a translucent reference for it.
	        */

	        return new RemoteReference(null, this, name, 0);
	  	}
	  	
	  	public function registerReference(object)
	  	{
	        /*
	        Get an ID for a local object.
	
	        Store a persistent reference to a local object and map its id()
	        to a generated, session-unique ID and return that ID.
			*/
	        if( object == null) throw new Error("Assertion Error - object is null")// None
	        var puid = object.processUniqueID();
	        var luid = this.luids[puid];
	        //trace("registerReference - puid = "+puid);
	        //trace("registerReference - luid = "+luid);
	        if (!luid)
	        {
	            if (__builtin__.len(this.localObjects) > MAX_BROKER_REFS)
	            {
	                this.maxBrokerRefsViolations = this.maxBrokerRefsViolations + 1
	                if (this.maxBrokerRefsViolations > 3)
	                {
	                    this.transport.loseConnection();
	                    throw new Error("Maximum PB reference count exceeded. Goodbye.");
	                }
	                throw new Error("Maximum PB reference count exceeded.");
	            }
	            luid = this.newLocalID();
	            this.localObjects[luid] = new Local(object);
	            this.luids[puid] = luid;
	        }
	        else
	        {
	            this.localObjects[luid].incref();
	        }
	        return luid;
	        
	    }
	    
	    public function setNameForLocal(name, object)
	    {
	        /*
	        Store a special (string) ID for this object.
	
	        This is how you specify a 'base' set of objects that the remote
	        protocol can connect to.
	        */
	        //assert object is not None
	        if( object == null) throw new Error("Assertion Error - object is null")
	        this.localObjects[name] = Local(object)
	    }
	}
}